Day4 提到,階段一要處理的問題是:當 AI 對 codebase 做了一次修改,我們如何控制它?在判斷這次修改「對不對」之前,還有兩件更基本的事:知道它改了什麼,以及改壞時能回到哪裡。 這就是版本控制的工作。
版本控制是對專案變化的系統性紀錄。它保存專案在各個時間點的狀態,讓我們能比較任兩個版本的差異、查閱修改的歷史,並在需要時回到先前的狀態。
Git 是目前最普遍的版本控制工具。在 Git 中,保存版本的基本單位是 commit:每個 commit 記錄當下的檔案狀態,以及作者、時間與說明。Git 的基本操作都在本機完成,不需要網路或 GitHub 帳號;GitHub 則是託管 Git 儲存庫的平台,多人協作時才會用到,留到明天再談。
想像一個情境:請 agent 實作一個功能,幾分鐘後它回報完成,總共改了 8 個檔案。執行之後,原本正常的功能壞了。
問題來了:是哪一處修改造成的?改之前的程式長什麼樣子?
人自己修改時,速度慢,範圍也小,多少還記得動過哪裡;agent 則在一次任務中橫跨多個檔案,修改的速度遠超過我們追蹤的速度。若沒有版本控制,能依賴的只剩兩種記憶:我們自己的,以及 agent 的。而 agent 對「原本的程式」的認知,只存在於它當下的 context 裡;Day3 談過,context 可能不完整、也可能已經被後來的修改覆蓋。
因此,版本控制的價值在於:把「改了什麼」與「改之前是什麼」從記憶中移出來,變成可以查閱的紀錄。
commit 之前,Git 會先讓我們選擇這次要保存的內容。修改先加入暫存區(staging area),再由 git commit 保存成一個版本:
git add todo.md
git commit -m "新增文章待辦清單"
暫存區存在的理由,是一次開發往往同時產生多項修改,但它們未必屬於同一個目的。先選擇、再提交,才能讓每個 commit 只包含一件事。
這件事比看起來重要:commit 的粒度,就是之後能回復的粒度。 若一個 commit 同時包含新功能與一段無關的重構,之後想撤銷其中一個,就只能連另一個一起撤銷。
在提交之前,或 agent 回報完成之後,第一步是看它改了什麼:
git status # 哪些檔案被修改、新增,哪些已暫存
git diff # 尚未暫存的修改內容
git diff --staged # 已暫存、將納入下一次提交的內容

檢查 agent 的修改時,diff 是成本最低的第一道 review。在細讀邏輯之前,先確認修改範圍:是否動到了與任務無關的檔案?是否刪掉了不該刪的內容?這些問題不需要理解程式,只要看 diff 就能發現。
也要注意,agent 在回報時給出的修改摘要,是它認為自己做了什麼;diff 則是實際發生了什麼。兩者不一致時,以 diff 為準。
當修改已經提交,可以用 git log 找到目標,再用 git revert 撤銷:
git log --oneline # 列出提交紀錄,找到要撤銷的 commit
git revert HEAD --no-edit # 撤銷最近一次提交
git revert 不會刪除歷史,而是新增一個反向套用的 commit。原本的修改與撤銷的紀錄都會保留,事後仍能追查「改過什麼、為什麼又改回來」。

若修改尚未提交,則可以用 git restore <file> 丟棄工作目錄中的修改。但這個操作無法復原,而且只能回到最近一次 commit 的狀態。換句話說,Git 只能保護已經提交的內容;沒有 commit 過的狀態,就沒有可以回去的地方。
完整的操作流程與 VS Code 介面,可以參考 Pro Git:Git 基礎 與 VS Code:Source Control。
上面三個角色,對應到與 agent 合作時的三個習慣:
git log 成為這次任務的工作紀錄。實際下指令時,可以這樣描述:
git revert 撤銷 commit a1b2c3d,完成後回報檔案變化與新增的提交紀錄。」Git 讓每次修改都能被看見、被回復,但哪些修改該保存、哪些該撤銷,仍然由我們決定。
到目前為止,都只有一條修改路線。當同事與 agent,或多個 agent 同時修改同一個專案時,又該如何整合?明天來談多人開發。